package com.gr.staffpm.security;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;

import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.gr.staffpm.datatypes.Permission;
import com.gr.staffpm.datatypes.Role;
import com.gr.staffpm.datatypes.User;
import com.gr.staffpm.security.service.UserService;

/**
 * StaffPM's one and only configured Apache Shiro Realm.
 */
public class SecurityRealm extends AuthorizingRealm {

    private final Logger log = LoggerFactory.getLogger(SecurityRealm.class);
    private UserService userService;

    /*--------------------------------------------
    |    I N S T A N C E   V A R I A B L E S    |
    ============================================*/

    protected boolean permissionsLookupEnabled = true;

    /*--------------------------------------------
    |         C O N S T R U C T O R S           |
    ============================================*/

    /*--------------------------------------------
    |  A C C E S S O R S / M O D I F I E R S    |
    ============================================*/

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    /**
     * Enables lookup of permissions during authorization.  The default is "false" - meaning that only roles
     * are associated with a user.  Set this to true in order to lookup roles <b>and</b> permissions.
     *
     * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only
     *                                 roles should be looked up.
     */
    public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
        this.permissionsLookupEnabled = permissionsLookupEnabled;
    }

    /*--------------------------------------------
    |               M E T H O D S               |
    ============================================*/

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();

        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }

        AuthenticationInfo info = null;

        String password = getPasswordForUser(username);

        if (password == null) {
            throw new UnknownAccountException("No account found for user [" + username + "]");
        }

        info = buildAuthenticationInfo(username, password.toCharArray());

        return info;
    }

    protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
        return new SimpleAuthenticationInfo(username, password, getName());
    }

    /**
     * This implementation of the interface expects the principals collection to return a String username keyed off of
     * this realm's {@link #getName() name}
     *
     * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        String username = (String) getAvailablePrincipal(principals);

        Set<String> roleNames = null;
        Set<String> permissions = null;

        // Retrieve roles and permissions from database
        roleNames = getRoleNamesForUser(username);
        if (permissionsLookupEnabled) {
            permissions = getPermissions(username, roleNames);
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        info.setStringPermissions(permissions);
        return info;

    }

    private String getPasswordForUser(String username) {

        User user = userService.findUser(username);

        if (user == null)
            return null;

        return user.getPassword();
    }

    protected Set<String> getRoleNamesForUser(String username) {

        Set<String> roleNames = new LinkedHashSet<String>();

        User user = userService.findUser(username);

        // Loop over results and add each returned role to a set
        for (Role role : user.getRoles()) {
            roleNames.add(role.getRole());
        }
        return roleNames;
    }

    protected Set<String> getPermissions(String username, Collection<String> roleNames) {

        Set<String> permissions = new LinkedHashSet<String>();

        User user = userService.findUser(username);

        for (Role role : user.getRoles()) {
            // Loop over results and add each returned role's permissions to a set
            for (Permission permission : role.getPermissions()) {
                // Add the permission to the set of permissions
                permissions.add(permission.getPermission());
            }

        }

        return permissions;
    }

}
